#!/usr/bin/python

"""
Class definitions for the low-level components of the architecture.
Element, Cell, and Cellfield.
"""

import sys
import struct

from utils import *
from logger import logger
from interconnect import Interconnect

class CellfieldError(Exception):
    """
    handling class for cellfield exceptions
    """

    def __init__(self, culprit, value):
        """
        initialize the class
        """
        logger.error("cellfield: %s: %s" % (culprit, value))

class Cellfield:
    """
    Cellfields are arrays of cells
    """

    def __init__(self, dimension = 4, proportion = 1, type = 'O'):
        """
        initialize the class
        """
        self.cellfield = []
        #must be an integers
        self.width = dimension
        self.height = int(dimension / proportion)
        self.set_type(type)

        #create the interconnect network for the cellfield
        self.interconnect = Interconnect(self.width, self.height)
    
    def set_type(self, type):
        for i in range(self.height):
            row = []
            for j in range(self.width):
                row.append(Cell(type))
            self.cellfield.append(row)

class CellError(Exception):
    """
    handling class for cell exceptions
    """

    def __init__(self, culprit, value):
        """
        initialize the class
        """
        logger.error("cell: %s: %s" % (culprit, value))

class Cell:
    """
    Cells are groups of elements
    """

    def __init__(self, type = 'O'):
        """
        initialize the cell
        """
        
        #initialize the celltype dictionary if it's empty
        if len(cell_type_dict) == 0:
            load_cell_lib()

        #declare member variables
        self.matrix = []
        self.interconnect = []
        self.type = ''
        
        #initialize the matrix
        self.set_type(type)
        
    def set_type(self, type):
        """
        set the type of the cell
        
        when the cell's type changes, its elements must be reconfigured
        """
        #update the cell type
        self.type = type
        
        #define the cell's elements according to the celltype dictionary
        if type not in cell_type_dict:
            raise CellError("cell type not defined", type)
        
        #the cell type definition contains a 2-D array of element types
        self.matrix = []
        for elemtype_row in cell_type_dict[type].elements:
            #build a row of elements
            matrix_row = []
            for elemtype in elemtype_row:
                #add an element of the specified type
                matrix_row.append(Element(elemtype))
            #add the row to the cell's matrix of elements
            self.matrix.append(matrix_row)
        
        #create the interconnect network for this cell
        self.interconnect = Interconnect(len(self.matrix[0]),
                                         len(self.matrix))

    def get_type(self):
        """
        get the type of the cell
        """
        return self.type

    def compute(self, a, b, c, d):
        """
        perform the cell computation
        """        
        #init the interconnect network with the inputs
        self.init_inputs(a, b, c, d)

        #loop to allow for propagation through the cell
        for delay in range(self.propogation_delay()):
            for row in range(len(self.matrix)):
                for col in range(len(self.matrix[0])):
                    #formulate the inputs to the LUT
                    lut_in = self.interconnect.get(row, col, 'ab')
                    if row == 0 or col >= row:
                        lut_in += self.interconnect.get(row, col, 'nw')
                        lut_in += self.interconnect.get(row, col, 'n')
                    elif col == 0:
                        lut_in += self.interconnect.get(row, col, 'n')
                        lut_in += self.interconnect.get(row, col, 'e')
                    else:
                        lut_in += self.interconnect.get(row, col, 'nw')
                        lut_in += self.interconnect.get(row, col, 'e')

                    #get and handle the outputs from the LUT
                    lut_out = self.matrix[row][col].lut(lut_in)
                    if row == 0 or col == 0 or col > row:
                        self.interconnect.set(row, col, 's', lut_out[0])
                        self.interconnect.set(row, col, 'se', lut_out[1])
                    else:
                        self.interconnect.set(row, col, 'w', lut_out[0])
                        self.interconnect.set(row, col, 'se', lut_out[1])
            
        return self.get_result()

    def init_inputs(self, a, b, c, d):
        """
        initialize the element interconnect network with the cell's inputs
        """
        numrows = len(self.matrix)
        numcols = len(self.matrix[0])
        
        # the a and b inputs get distributed throughout the network
        for row in range(numrows):
            for col in range(numcols):
                self.interconnect.set(row, col, 'ab', 
                                      a[col] + b[numrows - 1 - row])

        # the c and d inputs get sent to the first row
        for col in range(numcols):
            self.interconnect.set(0, col, 'nw', c[col])
            self.interconnect.set(0, col, 'n', d[col])
    
    def propogation_delay(self):
        """
        return the maximum (worst case) number of steps required for this cell 
        to complete a computation 
        """
        return len(self.matrix) + len(self.matrix[0]) - 1
        
    def get_result(self):
        """
        read the computed result from the element interconnect network
        """
        numrows = len(self.matrix)
        lastrowidx = numrows - 1
        numcols = len(self.matrix[0])
        lastcolidx = numcols - 1
        
        result = self.interconnect.get(numrows-1, 0, 's')

        for col in range(numcols - 1):
            result += self.interconnect.get(lastrowidx, col, 'se')
            
        for row in range(numrows):
            result += self.interconnect.get(lastrowidx - row, lastcolidx, 'se')
        
        return result

class ElementError(Exception):
    """
    handling class for element exceptions
    """
    
    def __init__(self, culprit, value):
        """
        initialize the class
        """
        logger.error("element: %s: %s" % (culprit, value))

class Element:
    """
    The element is the basic unit of the architecture.
    """

    def __init__(self, type = 'empty'):
        """
        initialize the class
        """
        #initialize the LUT dictionary if it is empty
        if len(element_type_dict) == 0:
            load_element_lib()

        #declare member variables
        self.type = ''
        self.element = []

        #initialize the element
        self.set_type(type)
    
    #math mode
    
    def set_type(self, type):
        """
        set the values of the element to act as a look-up table (LUT)
        """
        #update the element type
        self.type = type
        #arrange element according to the LUT dictionary
        if type not in element_type_dict:
            raise ElementError("LUT not defined", type)

        self.element = element_type_dict[type]
    
    def lut(self, input):
        """
        use the inputs to index the element as a look-up table (LUT)
        """
        try:
            row = self.element[int(input, 2)]
        except:
            raise ElementError("lut index out of range", int(input, 2))
        return ''.join(row)
    
    #memory mode
        
    def read(self, row, col):
        """
        read an entry in the element
        """
        #index is zero-based
        if row not in range(ROWS_PER_ELEMENT):
            raise ElementError("row out of range", row)
        elif col not in range(COLS_PER_ELEMENT):
            raise ElementError("col out of range", col)
        return self.element[row][col]

    def write(self, row, col, val):
        """
        write a value to the element
        """
        #index is zero-based
        if row not in range(ROWS_PER_ELEMENT):
            raise ElementError("row out of range", row)
        elif col not in range(COLS_PER_ELEMENT):
            raise ElementError("col out of range", col)
        #binary values can be 1 or 0
        elif val != '0' and val != '1':
            raise ElementError("val not binary", val)
        self.element[row][col] = val
